/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */

package vm;

import java.awt.Color;
import java.beans.XMLDecoder;
import java.beans.XMLEncoder;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.util.*;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.swing.JLabel;
import javax.swing.JOptionPane;
import javax.swing.JTextArea;
import javax.swing.SwingUtilities;
import javax.swing.text.BadLocationException;
import javax.swing.text.Highlighter.Highlight;

/**
 *
 * @author Peer
 */
public class hvmMachine {

    private final int maxMem = 16384;
    private int pc = 0;
    private String input = "", name = "";
    private JLabel output;
    private char instr = ' ';
    private int tmp;
    private int cycles;
    private boolean canGoOn = true, allowFire = true;
    private Highlight pcHighlight;
    private vmHighlightPainter pcHighlightPainter = new vmHighlightPainter(Color.ORANGE, null, Color.BLACK, vmHighlightPainter.DECO_BOTH);
    private hvm view = null;
    private JTextArea code = null;
    private LinkedList<Integer> stack = new LinkedList<Integer>();
    private ArrayDeque<Integer> callstack = new ArrayDeque<Integer>();
    private HashMap<Integer,Integer> memory = new HashMap<Integer,Integer>();
    private StackTableModel stackViewer;
    private MemoryTableModel memoryViewer;

    public hvmMachine(String name, JTextArea code, JLabel output) {
        this.name = name;
        this.code = code;
        this.output = output;
    }

    void push(int a) throws vmException {
        if (a > 2147483647 || a < -2147483648)
            throw new vmException("integer overflow");
        stack.push(a);
        fireStacksizeUpdated();
    }

    int pop() throws vmException {
        if(stack.size()==0)
            throw new vmException("stack underflow");
        int popvar = stack.pop();
        fireStacksizeUpdated();
        return popvar;
    }

    void reset() {
        if(!code.isEditable()) {            //machine is running
            canGoOn = false;                //trigger stop and
            while(!code.isEditable()) {     //wait for it
                try {
                    Thread.sleep(200);
                } catch (InterruptedException ex) {
                    Logger.getLogger(hvmMachine.class.getName()).log(Level.SEVERE, null, ex);
                }
            }
            canGoOn = true;
            return;                         //then exit reset()
        }
                                            //else reset the machine
        pc = 0;
        callstack.clear();
        stack.clear();
        memory.clear();
        restoreInput();
        output.setText(output.getName());
        canGoOn = true;
        cycles = 0;
        refreshView();
        removePCHighlight();
        broadcastStatus("\""+name+"\" reseted");
    }
    
    void run(final int limit) {
        new Thread() {
            @Override
            public void run() {
                if(!code.isEditable()) return;  //already running, don't run twice!
                code.setEditable(false);
                if(canGoOn)
                    broadcastStatus("Execution of HVM \""+name+"\" started");
                else
                    broadcastStatus("Cannot resume Execution of \""+name+"\"");
                while(canGoOn&&pc<limit) {
                    try {
                        runStep(false);
                        sleep(vm.getTimeout());
                    } catch (InterruptedException ex) {
                        Logger.getLogger(hvmMachine.class.getName()).log(Level.SEVERE, null, ex);
                    }
                }
                code.setEditable(true);
                code.getCaret().setVisible(true);
                code.requestFocusInWindow();
                refreshView();
            }
        }.start();
    }
    
    void runStep(boolean isSingleStep) {

        if(!canGoOn) {
            broadcastStatus("Cannot resume Execution of HVM \""+name+"\"");
            return;
        }
        if(isSingleStep || vm.getTimeout()>99)
            allowFire = true;
        else
            allowFire = false;
        try {
            if(++cycles>vm.getMaxCycles())
                throw new vmException("too many cycles");
            fireCyclesUpdated();
            instr = code.getText(pc++, 1).charAt(0);

            fireProgramcounterUpdated();
            //System.out.println(instr);
            switch (instr) {
                case '0': push(0); break;
                case '1': push(1); break;
                case '2': push(2); break;
                case '3': push(3); break;
                case '4': push(4); break;
                case '5': push(5); break;
                case '6': push(6); break;
                case '7': push(7); break;
                case '8': push(8); break;
                case '9': push(9); break;
                case '+': push(pop()+pop()); break;
                case '-': tmp = pop(); push(pop()-tmp); break;
                case '*': push(pop()*pop()); break;
                case '/': tmp = pop(); push(pop()/tmp); break;
                case 'p': out(Integer.toString(pop())); break;
                case 'P': out(String.copyValueOf(Character.toChars(pop()&0x7F))); break;
                case ':': tmp = pop(); push(Integer.signum(pop()-tmp)); break;
                case 'g': pc+=pop(); fireProgramcounterUpdated(); break;
                case '?': tmp = pop(); if(pop()==0) pc+=tmp; fireProgramcounterUpdated(); break;
                case 'c': callstack.push(pc); pc = pop(); fireCallstacksizeUpdated(); fireProgramcounterUpdated(); break;
                case '$': if(callstack.size()==0) throw new vmException("call stack underflow");
                          pc=callstack.pop(); fireCallstacksizeUpdated(); fireProgramcounterUpdated(); break;
                case '<': tmp = pop(); if(tmp<0 || tmp>maxMem) throw new vmException("memory read access violation @"+tmp);
                          if(memory.containsKey(tmp)) push(memory.get(tmp)); else push(0); fireMemoryUpdated(); break;
                case '>': tmp = pop(); if(tmp<0 || tmp>maxMem) throw new vmException("memory write access violation @"+tmp);
                          memory.put(tmp, pop()); fireMemoryUpdated(); break;
                case '^': push(stack.get(pop())); break;
                case 'v': push(stack.remove(pop())); break;
                case 'd': pop(); break;
                case '!': canGoOn = false;
                //case '\n': runStep(true);
            }
            if(isSingleStep)
                broadcastStatus("Gone one Step in \""+name+"\"");
            if(isSingleStep || vm.getTimeout()>99) {
                SwingUtilities.invokeLater(new Runnable() {
                    public void run() {
                        if(pcHighlight==null)
                            try {
                                pcHighlight = (Highlight)code.getHighlighter().addHighlight(pc, pc + 1, pcHighlightPainter);
                            } catch (BadLocationException ex) {
                                Logger.getLogger(hvmMachine.class.getName()).log(Level.SEVERE, null, ex);
                            }
                        else
                            try {
                                code.getHighlighter().changeHighlight(pcHighlight, pc, pc + 1);
                            } catch (BadLocationException ex) {
                                Logger.getLogger(hvmMachine.class.getName()).log(Level.SEVERE, null, ex);
                            }
                    }
                });
            } else
                removePCHighlight();
            if(pc==code.getDocument().getLength())
                canGoOn=false;
            if(!canGoOn)
                done();
        } catch (Exception ex) {
            broadcastStatus("Exception: "+ex.getMessage());
            JOptionPane.showConfirmDialog(code.getParent(),
                                          ex.getMessage(),
                                          "Exception",
                                          JOptionPane.DEFAULT_OPTION,
                                          JOptionPane.ERROR_MESSAGE);
            canGoOn = false;
            removePCHighlight();
        }
    }

    void setInput(String input) {
        this.input = input;
        restoreInput();
    }

    private void restoreInput() {
        memory.clear();
        Scanner scan = new Scanner(input).useDelimiter(",");
        for (int i = 0; i < maxMem; i++) {
            if(scan.hasNext())
                memory.put(Integer.valueOf(i), Integer.valueOf(scan.next()));
            else
                break;
        }
        fireMemoryUpdated();
    }

    void out(String out) {
        output.setText(output.getText()+out);
    }

    private void done() {
        removePCHighlight();
        refreshView();
        broadcastStatus("Execution of HVM \""+name+"\" finished");
        JOptionPane.showConfirmDialog(code.getParent(),
                                      "<html>HVM \""+name+"\" has finished execution.<br>"+output.getText()+"<html>",
                                      "Done",
                                      JOptionPane.DEFAULT_OPTION,
                                      JOptionPane.INFORMATION_MESSAGE);
    }

    public void setName(String name) {
        this.name = name;
    }

    private void fireCyclesUpdated(){
        if(view!=null)
            view.cyclesUpdated(cycles);
    }

    private void fireCallstacksizeUpdated() {
        if(view!=null)
            view.callstacksizeUpdated(callstack.size());
    }

    private void fireStacksizeUpdated() {
        if(allowFire && view!=null)
            view.stacksizeUpdated(stack.size());
        if(allowFire && stackViewer!=null)
        SwingUtilities.invokeLater(new Runnable() {
            public void run() {
                stackViewer.fireTableStructureChanged();
            }
        });
            
    }

    private void fireProgramcounterUpdated() {
        if(view!=null)
            view.programcounterUpdated(pc);
    }

    private void fireMemoryUpdated() {
        if(allowFire && memoryViewer!=null)
        SwingUtilities.invokeLater(new Runnable() {
            public void run() {
                memoryViewer.fireTableStructureChanged();
            }
        });
    }

    public void registerView(hvm view) {
        this.view = view;
    }

    public void registerStackViewer(StackTableModel stackViewer) {
        this.stackViewer = stackViewer;
        stackViewer.setData(stack);
    }

    public void registerMemoryViewer(MemoryTableModel memoryViewer) {
        this.memoryViewer = memoryViewer;
        memoryViewer.setData(memory);
    }

    public void broadcastStatus(String out) {
        if(view!=null) {
            view.broadcastStatus(out);
        }
    }

    private void refreshView() {
        allowFire=true;
        fireCallstacksizeUpdated();
        fireStacksizeUpdated();
        fireCyclesUpdated();
        fireProgramcounterUpdated();
        fireMemoryUpdated();
    }

    void save() {
        try {
            XMLEncoder e = new XMLEncoder(new BufferedOutputStream(new FileOutputStream("machines/"+name + ".hvm")));
            e.writeObject(new String("HVM"));
            e.writeObject(this.code.getText());
            e.writeObject(this.input);
            e.close();
        }
        catch (FileNotFoundException ex) {
            Logger.getLogger(hvm.class.getName()).log(Level.SEVERE, null, ex);
        }
        broadcastStatus("HVM \""+name+"\" saved");
    }

    void load() {
        try {
            XMLDecoder d = new XMLDecoder(new BufferedInputStream(new FileInputStream("machines/" + name + ".hvm")));
            if(d.readObject().equals("HVM")) {
                code.setText((String)d.readObject());
                String in =(String)d.readObject();
                if(view!=null)
                    view.setInput(in);
                else
                    setInput(in);
            }
            d.close();
        } catch (FileNotFoundException ex) {
            Logger.getLogger(hvmMachine.class.getName()).log(Level.SEVERE, null, ex);
            broadcastStatus("Error loading HVM \""+name+"\"");
        }
        broadcastStatus("HVM \""+name+"\" loaded");
    }

    void updatePCHighlight() {
        if(pcHighlight!=null)
            try {
                code.getHighlighter().changeHighlight(pcHighlight, pc, pc + 1);
            } catch (BadLocationException ex) {
                Logger.getLogger(hvmMachine.class.getName()).log(Level.SEVERE, null, ex);
            }
        /*else if(code.getDocument().getLength()>0)
        try {
        pcHighlight = code.getHighlighter().addHighlight(0, 1, pcHighlightPainter);
        } catch (BadLocationException ex) {
        Logger.getLogger(hvmMachine.class.getName()).log(Level.SEVERE, null, ex);
        }*/
    }

    void removePCHighlight() {
        SwingUtilities.invokeLater(new Runnable() {
            public void run() {
                if(pcHighlight!=null) {
                    code.getHighlighter().removeHighlight(pcHighlight);
                    pcHighlight = null;
                }
            }
        });
    }
}
